%%% Copyright 2010 Google Inc.
%%%
%%% Licensed under the Apache License, Version 2.0 (the "License");
%%% you may not use this file except in compliance with the License.
%%% You may obtain a copy of the License at
%%%
%%%     http://www.apache.org/licenses/LICENSE-2.0
%%%
%%% Unless required by applicable law or agreed to in writing, software
%%% distributed under the License is distributed on an "AS IS" BASIS,
%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%%% See the License for the specific language governing permissions and
%%% limitations under the License.

%%% @copyright 2010 Google Inc. All Rights Reserved.
%%% @author <jacoby@google.com> Jacoby Thwaites [http://sites/madcloud]
%%% @version {@version}
%%% @doc
%%% Handler for callbacks from the txm supervised inets:httpd server
%%%
%%% This implements the backend for the admin UI. XML output by this
%%% module is transformed to HTML5+CSS3 by the XSLT files in the ../priv
%%% directory.
%%%
%%% @headerfile "../include/txm.hrl"

-module(web_srv).

-include("struct.hrl").
-include("txm.hrl").
-include("view.hrl").
-include("web_srv.hrl").

-import(web_util, [client_redirect/2,
                   list_to_term/1,
                   query2proplist/1,
                   send_response/3]).

-export([new_folder/3, new_field/3, new_op/3]).
-export([delete/3]).
-export([record/3, contents/3, composition/3]).
-export([snapshot/3]).
-export([active_views/3]).
-export([upload/3]).

%% @doc Handles record request. If only the `path' query param is present,
%% then resolves from root. If the `root' query param is present,
%% then the path is relative to the supplied root (for example, the
%% the root folder of a prog/op.
%% @spec record(term(), list(), list()) -> ok
record(SessionId, _Env, Input) ->

  % Retrieve path and optional root
  Args = query2proplist(Input),
  try
    Root = proplists:get_value(?QUERY_ROOT, Args),
    Path = proplists:get_value(?QUERY_PATH, Args),
    ?OK_RETURN(Record) =
      case {Root, Path} of
        {undefined, Path} ->
          prog_srv:get_record(Path);
        {Root, undefined} ->
          prog_srv:get_record(list_to_term(Root));
        {Root, Path} ->
          prog_srv:get_record(list_to_term(Root), Path)
      end,
    send_response(Args, SessionId,
                  {record, Args,
                   [struct:record_to_struct(Record)]})
  catch
    Fault:Reason ->
      ?ERROR([{Fault, Reason}]),
      send_response(Args, SessionId,
                    {record, Args, [{error, [{Fault, Reason}]}]})
  end.

%% @doc Handles contents request. Returns the contents of the specified
%% folder as an index and list of record structs.
%% @spec contents(term(), list(), list()) -> ok
contents(SessionId, _Env, Input) ->
  Args = query2proplist(Input),
  try
    Root = proplists:get_value(?QUERY_ROOT, Args),
    Ref = list_to_term(Root),

    ?OK_RETURN(Index) = prog_srv:get_shallow_index(Ref),
    ?OK_RETURN(Records) = prog_srv:get_shallow_contents(Ref),
    Structs = [struct:record_to_struct(Record) || Record <- Records],
    Response = {contents, Args, [Index | Structs]},
    send_response(Args, SessionId, Response)
  catch
    Fault:Reason ->
      ?ERROR([{Fault, Reason}]),
      send_response(Args, SessionId,
                    {contents, Args, [{error, [{Fault, Reason}]}]})
  end.

%% @doc Handles composition request. Returns the contents of the specified
%% folder and its descendants as a nested index and list of record structs.
%% @spec composition(term(), list(), list()) -> ok
composition(SessionId, _Env, Input) ->
  Args = query2proplist(Input),
  try
    Root = proplists:get_value(?QUERY_ROOT, Args),
    Ref = list_to_term(Root),

    ?OK_RETURN(Index) = prog_srv:get_deep_index(Ref),
    ?OK_RETURN(Records) = prog_srv:get_deep_contents(Ref),
    Structs = [struct:record_to_struct(Record) || Record <- Records],
    Response = {?PROP_COMPOSITION, Args, [Index | Structs]},
    send_response(Args, SessionId, Response)
  catch
    Fault:Reason ->
      ?ERROR([{Fault, Reason}]),
      send_response(Args, SessionId,
                    {composition, Args, [{error, [{Fault, Reason}]}]})
  end.

%% @doc Handles new folder request, which is normally a POST submission.
%% In this and other POST submissions, we do a client-side redirect to
%% display the new listing, this prevents refresh and back button silliness.
%% @spec new_folder(term(), list(), list()) -> ok
new_folder(SessionId, Env, Input) ->

  % Retrieve root (ie containing folder) and name.
  Args = query2proplist(Input),
  try
    RootString = proplists:get_value(?QUERY_ROOT, Args),
    Root = list_to_term(RootString),
    Name = proplists:get_value(?QUERY_NAME, Args),
    ?OK_RETURN(#?FOLDER{}) = prog_srv:new_record(Root, Name, #?FOLDER{}),
    client_redirect(SessionId,
                    proplists:get_value(?ENV_HTTP_REFERER, Env))
  catch
    Fault:Reason ->
      ?ERROR([{Fault, Reason}]),
      send_response(Args, SessionId,
                    {new_folder, Args, [{error, [{Fault, Reason}]}]})
  end.

%% @doc Handles new field request, which is normally a POST submission.
%% The field name text can be a comma-separated list of names in which
%% case more than one field of the same type is created.
%% @spec new_field(term(), list(), list()) -> ok
new_field(SessionId, Env, Input) ->

  % Retrieve root (ie containing folder) and name.
  Args = query2proplist(Input),
  try
    RootString = proplists:get_value(?QUERY_ROOT, Args),
    Root = list_to_term(RootString),
    Names = string:tokens(proplists:get_value(?QUERY_NAME, Args), 
                          ?NAME_SEPARATOR),
    Type = proplists:get_value(?QUERY_TYPE, Args),

    % Fold the new record function across the names in the list.
    lists:foldl(fun(Name, Acc) ->
                     ?OK_RETURN(#?FIELD{}) =
                       prog_srv:new_record(Root, Name, #?FIELD{type=Type}),
                     Acc
                end,
                [], Names),
    client_redirect(SessionId,
                    proplists:get_value(?ENV_HTTP_REFERER, Env))
  catch
    Fault:Reason ->
      ?ERROR([{Fault, Reason}]),
      send_response(Args, SessionId,
                    {new_field, Args, [{error, [{Fault, Reason}]}]})
  end.

%% @doc Handles new op request, which is normally a POST submission.
%% @spec new_op(term(), list(), list()) -> ok
new_op(SessionId, Env, Input) ->

  % Retrieve parameters we use to construct the op.
  Args = query2proplist(Input),
  try
    RootString = proplists:get_value(?QUERY_ROOT, Args),
    Root = list_to_term(RootString),
    Name = proplists:get_value(?QUERY_NAME, Args),
    MaskDict = args2dict(Args, ?QUERY_MASK_SET),
    InputDict = args2dict(Args, ?QUERY_INPUT_SET),
    OutputDicts = props2outputs(Args),
    IsComposite =
      proplists:get_value(?QUERY_IS_COMPOSITE, Args) == ?QUERY_VALUE_TRUE,

    % Create the new op record and redirect back to the page the post came from.
    ?OK_RETURN(#?OP{}) =
      prog_srv:new_record(Root, Name,
                          #?OP{mask = nr_set:from_dict_key_name(MaskDict),
                               input = nr_set:from_dict_key_name(InputDict),
                               outputs = nr_set_dict:from_dict(OutputDicts),
                               isComposite = IsComposite}),
    client_redirect(SessionId,
                    proplists:get_value(?ENV_HTTP_REFERER, Env))
  catch
    Fault:Reason ->
      ?ERROR([{Fault, Reason}]),
      send_response(Args, SessionId,
                    {new_op, Args, [{error, [{Fault, Reason}]}]})
  end.

%% @doc Deletes a record reference from a folder.
%% @spec delete(term(), list(), list()) -> ok
delete(SessionId, Env, Input) ->
  Args = query2proplist(Input),
  try
    RootRef = list_to_term(proplists:get_value(?QUERY_ROOT, Args)),
    Name = proplists:get_value(?QUERY_NAME, Args),
    prog_srv:delete_ref(RootRef, Name),
    client_redirect(SessionId, proplists:get_value(?ENV_HTTP_REFERER, Env))
  catch
    Fault:Reason ->
      ?ERROR([{Fault, Reason}]),
      send_response(Args, SessionId,
                    {delete, Args, [{error, [{Fault, Reason}]}]})
  end.

%% @doc Snapshots a composite prog/op.
%% @spec snapshot(term(), list(), list()) -> ok
snapshot(SessionId, Env, Input) ->
  Args = query2proplist(Input),
  try
    RootString = proplists:get_value(?QUERY_ROOT, Args),
    OpRef = list_to_term(RootString),
    VsnName = proplists:get_value(?QUERY_NAME, Args),

    % Create the snapshot.
    prog_srv:snapshot(OpRef, VsnName),
    client_redirect(SessionId,
                    proplists:get_value(?ENV_HTTP_REFERER, Env))
  catch
    Fault:Reason ->
      ?ERROR([{Fault, Reason}]),
      send_response(Args, SessionId,
                    {snapshot, Args, [{error, [{Fault, Reason}]}]})
  end.

%% @doc Returns the active views on a composition folder.
%% @spec active_views(term(), list(), list()) -> ok
active_views(SessionId, _Env, Input) ->
  Args = query2proplist(Input),
  try
    RootString = proplists:get_value(?QUERY_ROOT, Args),
    Cookie = proplists:get_value(?QUERY_COOKIE, Args, ?ANY_COOKIE),
    CompRef = list_to_term(RootString),

    % Get the active views struct.
    ?OK_RETURN(Structs) = view_db:active_views(CompRef, Cookie),

    Response = {active_views, Args, Structs},
    send_response(Args, SessionId, Response)
  catch
    Fault:Reason ->
      ?ERROR([{Fault, Reason}]),
      send_response(Args, SessionId,
                    {active_views, Args, [{error, [{Fault, Reason}]}]})
  end.

%% @doc Imports the XML file into the prog database at location `Root'.
%% @spec upload(term(), list(), list()) -> ok
upload(SessionId, Env, Input) ->
  Args = [{"upload", "XML"}],
  try

    % Match the ContentType header.
    ?CONTENT_TYPE_MULTIPART ++ _More =
      ContentType = proplists:get_value(http_content_type, Env),

    % Use a regex to extract the boundary attribute in the header.
    {match, [Boundary]} =
      re:run(ContentType, "boundary=(.+)$", [{capture, all_but_first, list}]),

    % Split the multipart into a list of parts, and get the root folder ref
    % and XML text.
    Parts = web_util:parts(Input, Boundary),
    RootString = web_util:form_data(Parts, ?QUERY_ROOT),
    Root = list_to_term(RootString),
    Xml = web_util:form_data(Parts, ?QUERY_FILE),

    % Import the XML into the prog database and redirect the client back to the
    % referring page.
    prog_srv:import_binary(Root, Xml),
    client_redirect(SessionId,
                    proplists:get_value(?ENV_HTTP_REFERER, Env))
  catch
    Fault:Reason ->
      ?ERROR([{Fault, Reason}]),
      send_response(Args, SessionId,
                    {upload, Args, [{error, [{Fault, Reason}]}]})
  end.

%%% ---------------------------------------------------------------------------
%%% Supporting functions.
%%% ---------------------------------------------------------------------------

%% Resolves the space-separated list of hrefs in the `Param' entry of `Args'
%% into a dictionary of href -> ref().
%% @spec args2dict(proplist(), name()) -> dict()
args2dict(Args, Param) ->
  RootRef = list_to_term(proplists:get_value(?QUERY_ROOT, Args)),
  Line = proplists:get_value(Param, Args),
  line2dict(RootRef, Line).

%% Resolves the space-separated list of hrefs in `Line' into a dict of name
%% to ref, resolving relative to `RootRef'.
%% @spec line2dict(ref(), string()) -> dict()
line2dict(RootRef, Line) ->
  Names = string:tokens(Line, ?NAME_SEPARATOR),
  resolve_names(RootRef, Names).

%% Resolves the list of scoped names in `Names' against `FolderRef' and
%% returns a dictionary of name to ref().
%% @spec resolve_names(ref(), list()) -> dict()
resolve_names(FolderRef, Names) ->
  dict:from_list(
    [{Name, ?OK_RESULT(prog_srv:resolve_scoped(FolderRef, Name))}
     || Name <- Names]).

%% Reads the proplist and generates a set of output set records.
%% Each line is an output set, comprising `setname:field1,field2'.
%% @spec props2outputs(list()) -> dict()
props2outputs(Args) ->

  % Get hold of the parent folder's entries.
  RootRef = list_to_term(proplists:get_value(?QUERY_ROOT, Args)),

  % Outputs consist of one line per output set, each line having a name, a
  % colon, and then a comma separated list of field references.
  Lines = string:tokens(proplists:get_value(?QUERY_OUTPUT_SETS, Args),
                        ?HTTP_LINE_BREAK),

  % Stage1 is something like [["key1","field1 field2"], ..]
  Stage1 = [string:tokens(Line, ":") || Line <- Lines],

  % Stage2 converts each ["key1","field1 field2"] element into a
  % {"key1", dict()} element.
  Stage2 = lists:foldl(fun([OutputKey, FieldsLine], Acc) ->
                   Names = string:tokens(FieldsLine, ?NAME_SEPARATOR),
                   [{OutputKey, resolve_names(RootRef, Names)} | Acc]
              end,
              [], Stage1),

  % Returns a dictionary from the {key,value} pairs.
  dict:from_list(Stage2).
